/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.impl.wsdl.support.wsa;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.AnonymousTypeConfig;
import com.eviware.soapui.impl.wsdl.WsdlOperation;
import com.eviware.soapui.impl.wsdl.panels.teststeps.support.WsaAssertionConfiguration;
import com.eviware.soapui.impl.wsdl.submit.WsdlMessageExchange;
import com.eviware.soapui.impl.wsdl.support.soap.SoapUtils;
import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlUtils;
import com.eviware.soapui.model.testsuite.AssertionError;
import com.eviware.soapui.model.testsuite.AssertionException;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.xml.XmlUtils;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Validating class for WS Addressing implemented according to WSDL 1.1
 * specification
 *
 * @author dragica.soldo
 * @see {@link}http://www.w3.org/TR/2006/WD-ws-addr-wsdl-20060216/#WSDL11MEPS
 */
public class WsaValidator {
    WsdlMessageExchange messageExchange;
    Element header;
    String wsaVersionNameSpace;
    StringBuilder cumulativeErrorMsg;
    WsaAssertionConfiguration wsaAssertionConfiguration;

    public WsaValidator(WsdlMessageExchange messageExchange, WsaAssertionConfiguration wsaAssertionConfiguration) {
        this.messageExchange = messageExchange;
        this.wsaAssertionConfiguration = wsaAssertionConfiguration;
        cumulativeErrorMsg = new StringBuilder();
    }

    public static String getWsaVersion(XmlObject contentObject, SoapVersion soapVersion) {
        String wsaVns = null;
        try {
            // XmlObject xmlObject = XmlObject.Factory.parse( content );
            XmlObject[] envS = contentObject.selectChildren(soapVersion.getEnvelopeQName());
            Element envelope = (Element) envS[0].getDomNode();

            Element hdr = (Element) SoapUtils.getHeaderElement(contentObject, soapVersion, true).getDomNode();

            if (!hdr.hasChildNodes()) {
                return null;
            }

            String wsaNameSpace = XmlUtils.findPrefixForNamespace(hdr, WsaUtils.WS_A_NAMESPACE_200508);
            if (wsaNameSpace != null) {
                wsaVns = WsaUtils.WS_A_NAMESPACE_200508;
            } else {
                wsaNameSpace = XmlUtils.findPrefixForNamespace(hdr, WsaUtils.WS_A_NAMESPACE_200408);
                if (wsaNameSpace != null) {
                    wsaVns = WsaUtils.WS_A_NAMESPACE_200408;
                } else {
                    wsaNameSpace = XmlUtils.findPrefixForNamespace(envelope, WsaUtils.WS_A_NAMESPACE_200508);
                    if (wsaNameSpace != null) {
                        wsaVns = WsaUtils.WS_A_NAMESPACE_200508;
                    } else {
                        wsaNameSpace = XmlUtils.findPrefixForNamespace(envelope, WsaUtils.WS_A_NAMESPACE_200408);
                        if (wsaNameSpace != null) {
                            wsaVns = WsaUtils.WS_A_NAMESPACE_200408;
                        } else {
                            return null;
                        }
                    }
                }
            }
        } catch (XmlException e) {
            SoapUI.logError(e);
        }
        return wsaVns;
    }

    private void validateWsAddressingCommon(String content) {
        if (wsaAssertionConfiguration.isAssertTo()) {
            Element toNode;
            if (wsaVersionNameSpace != null) {
                toNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, "To");
                parseToNode(toNode);
            } else {
                toNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200508, "To");
                if (toNode == null) {
                    toNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200408, "To");
                }
                parseToNode(toNode);
            }
        }
        // if fault_to is specified check if anonymous allowed; faultTo is never
        // asserted
        Element faultToNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, "FaultTo");
        if (faultToNode != null) {
            Element addressNode = XmlUtils.getFirstChildElementNS(faultToNode, wsaVersionNameSpace, "Address");
            if (addressNode != null) {
                String faultToAddressValue = XmlUtils.getElementText(addressNode);
                if (!StringUtils.isNullOrEmpty(faultToAddressValue)) {
                    // check for anonymous
                    if (AnonymousTypeConfig.PROHIBITED.toString().equals(messageExchange.getOperation().getAnonymous())
                            && WsaUtils.isAnonymousAddress(faultToAddressValue, wsaVersionNameSpace)) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader FaultTo , Anonymous addresses are prohibited. ");
                    } else if (AnonymousTypeConfig.REQUIRED.toString().equals(
                            ((WsdlMessageExchange) messageExchange).getOperation().getAnonymous())
                            && !(WsaUtils.isAnonymousAddress(faultToAddressValue, wsaVersionNameSpace) || WsaUtils
                            .isNoneAddress(faultToAddressValue, wsaVersionNameSpace))) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader FaultTo , Anonymous addresses are required. ");
                    }
                }
            }
        }

    }

    private void parseToNode(Element toNode) {
        if (toNode == null) {
            cumulativeErrorMsg.append("WS-A To property is not specified. ");
        } else {
            String toAddressValue = XmlUtils.getElementText(toNode);
            if (StringUtils.isNullOrEmpty(toAddressValue)) {
                cumulativeErrorMsg.append("WS-A To property is empty. ");
            } else {
                // check for anonymous - in case of mock response to=request.replyTo
                if (AnonymousTypeConfig.PROHIBITED.toString().equals(messageExchange.getOperation().getAnonymous())
                        && WsaUtils.isAnonymousAddress(toAddressValue, wsaVersionNameSpace)) {
                    cumulativeErrorMsg.append("WS-A InvalidAddressingHeader To , Anonymous addresses are prohibited. ");
                }
            }
        }
    }

    public void validateWsAddressingRequest() throws AssertionException, XmlException {
        String content = messageExchange.getRequestContent();
        SoapVersion soapVersion = messageExchange.getOperation().getInterface().getSoapVersion();

        // XmlObject xmlObject = XmlObject.Factory.parse( content );
        XmlObject xmlObject = XmlUtils.createXmlObject(content);
        header = (Element) SoapUtils.getHeaderElement(xmlObject, soapVersion, true).getDomNode();

        wsaVersionNameSpace = getWsaVersion(xmlObject, soapVersion);
        // not checking because of possibility of having wsaVersionNamespace
        // specified inside property tag itself
        // if (wsaVersionNameSpace == null)
        // {
        // throw new AssertionException( new AssertionError( "WS-A not enabled" )
        // );
        // }

        WsdlOperation operation = messageExchange.getOperation();

        if (wsaAssertionConfiguration.isAssertAction()) {
            assertProperty("Wsa:Action", "Action");
        }
        validateWsAddressingCommon(content);
        if (operation.isRequestResponse()) {
            if (wsaAssertionConfiguration.isAssertMessageId()) {
                // MessageId is Mandatory
                assertProperty("Wsa:MessageId", "MessageId");
            }
            if (wsaAssertionConfiguration.isAssertReplyTo()) {
                // ReplyTo is Mandatory
                Element replyToNode;
                String currentTagWsaNs;
                if (!StringUtils.isNullOrEmpty(wsaVersionNameSpace)) {
                    replyToNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, "ReplyTo");
                    parseReplyToNode(replyToNode, wsaVersionNameSpace);
                } else {
                    replyToNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200508, "ReplyTo");
                    currentTagWsaNs = WsaUtils.WS_A_NAMESPACE_200508;
                    if (replyToNode == null) {
                        replyToNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200408, "ReplyTo");
                        currentTagWsaNs = WsaUtils.WS_A_NAMESPACE_200408;
                    }
                    parseReplyToNode(replyToNode, currentTagWsaNs);
                }
            }
        }
        String cumulativeError = cumulativeErrorMsg.toString();
        if (!StringUtils.isNullOrEmpty(cumulativeError)) {
            throw new AssertionException(new AssertionError(cumulativeError));
        }
    }

    private void parseReplyToNode(Element replyToNode, String wsaNsStr) {
        if (replyToNode == null) {
            cumulativeErrorMsg.append("WS-A ReplyTo property is not specified. ");
        } else {
            Element addressNode = XmlUtils.getFirstChildElementNS(replyToNode, wsaNsStr, "Address");
            if (addressNode == null) {
                cumulativeErrorMsg.append("WS-A ReplyTo Address property is not specified. ");
            } else {
                String replyToAddressValue = XmlUtils.getElementText(addressNode);
                if (StringUtils.isNullOrEmpty(replyToAddressValue)) {
                    cumulativeErrorMsg.append("WS-A ReplyTo Address property is empty. ");
                } else {
                    // check for anonymous
                    if (AnonymousTypeConfig.PROHIBITED.toString().equals(
                            ((WsdlMessageExchange) messageExchange).getOperation().getAnonymous())
                            && WsaUtils.isAnonymousAddress(replyToAddressValue, wsaNsStr)) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader ReplyTo , Anonymous addresses are prohibited. ");
                    } else if (AnonymousTypeConfig.REQUIRED.toString().equals(
                            ((WsdlMessageExchange) messageExchange).getOperation().getAnonymous())
                            && !(WsaUtils.isAnonymousAddress(replyToAddressValue, wsaNsStr) || WsaUtils.isNoneAddress(
                            replyToAddressValue, wsaNsStr))) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader ReplyTo , Anonymous addresses are required. ");
                    }
                }
            }
        }
    }

    public void validateWsAddressingResponse() throws AssertionException, XmlException {
        String content = messageExchange.getResponseContent();
        SoapVersion soapVersion = messageExchange.getOperation().getInterface().getSoapVersion();

        // XmlObject requestXmlObject = XmlObject.Factory.parse(
        // messageExchange.getRequestContent() );
        XmlObject requestXmlObject = XmlUtils.createXmlObject(messageExchange.getRequestContent());
        // XmlObject xmlObject = XmlObject.Factory.parse( content );
        XmlObject xmlObject = XmlUtils.createXmlObject(content);
        header = (Element) SoapUtils.getHeaderElement(xmlObject, soapVersion, true).getDomNode();

        wsaVersionNameSpace = getWsaVersion(xmlObject, soapVersion);

        if (wsaAssertionConfiguration.isAssertAction()) {
            String defaultWsdlAction = WsdlUtils.getDefaultWsaAction(messageExchange.getOperation(), true);
            // Wsa:Action is assertion text, not property tag
            assertProperty("Wsa:Action", "Action", defaultWsdlAction);
        }
        validateWsAddressingCommon(content);

        if (wsaAssertionConfiguration.isAssertRelatesTo()) {
            // RelatesTo is Mandatory
            Element relatesToNode;
            if (!StringUtils.isNullOrEmpty(wsaVersionNameSpace)) {
                relatesToNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, "RelatesTo");
                parseRelatesToNode(soapVersion, requestXmlObject, relatesToNode);
            } else {
                relatesToNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200508, "RelatesTo");
                if (relatesToNode == null) {
                    relatesToNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200408, "RelatesTo");
                }
                parseRelatesToNode(soapVersion, requestXmlObject, relatesToNode);
            }
        }
        // if fault_to is specified check if anonymous allowed
        Element replyToNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, "ReplyTo");
        if (replyToNode != null) {
            Element addressNode = XmlUtils.getFirstChildElementNS(replyToNode, wsaVersionNameSpace, "Address");
            if (addressNode != null) {
                String replyToAddressValue = XmlUtils.getElementText(addressNode);
                if (!StringUtils.isNullOrEmpty(replyToAddressValue)) {
                    // check for anonymous
                    if (AnonymousTypeConfig.PROHIBITED.toString().equals(
                            ((WsdlMessageExchange) messageExchange).getOperation().getAnonymous())
                            && WsaUtils.isAnonymousAddress(replyToAddressValue, wsaVersionNameSpace)) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader ReplyTo , Anonymous addresses are prohibited. ");
                    } else if (AnonymousTypeConfig.REQUIRED.toString().equals(
                            ((WsdlMessageExchange) messageExchange).getOperation().getAnonymous())
                            && !(WsaUtils.isAnonymousAddress(replyToAddressValue, wsaVersionNameSpace) || WsaUtils
                            .isNoneAddress(replyToAddressValue, wsaVersionNameSpace))) {
                        cumulativeErrorMsg
                                .append("WS-A InvalidAddressingHeader ReplyTo , Anonymous addresses are required. ");
                    }
                }
            }
        }
        if (wsaAssertionConfiguration.isAssertReplyToRefParams()) {
            // check if request ReplyTo ReferenceParameters are included in
            // response
            NodeList requestReplyToRefProps = WsdlUtils.getRequestReplyToRefProps(messageExchange,
                    getWsaVersion(requestXmlObject, soapVersion));
            for (int i = 0; i < requestReplyToRefProps.getLength(); i++) {
                Node refProp = requestReplyToRefProps.item(i);
                String refPropName = refProp.getNodeName();
                NodeList existingResponseRefs = XmlUtils.getChildElementsByTagName(header, refPropName);
                if (existingResponseRefs != null && existingResponseRefs.getLength() > 0) {
                    // TODO check if tag is well formed: wsa:IsReferenceParameter
                    continue;
                } else {
                    cumulativeErrorMsg.append("Response does not have request ReferenceProperty " + refPropName + ". ");
                }

            }
        }
        if (wsaAssertionConfiguration.isAssertFaultToRefParams()) {
            // check if request FaultTo ReferenceParameters are included in
            // response
            NodeList requestFaultToRefProps = WsdlUtils.getRequestFaultToRefProps(messageExchange,
                    getWsaVersion(requestXmlObject, soapVersion));
            for (int i = 0; i < requestFaultToRefProps.getLength(); i++) {
                Node refProp = requestFaultToRefProps.item(i);
                String refPropName = refProp.getNodeName();
                NodeList existingResponseRefs = XmlUtils.getChildElementsByTagName(header, refPropName);
                if (existingResponseRefs != null && existingResponseRefs.getLength() > 0) {
                    continue;
                } else {
                    cumulativeErrorMsg.append("Response does not have request ReferenceProperty " + refPropName + ". ");
                }

            }
        }
        String cumulativeError = cumulativeErrorMsg.toString();
        if (!StringUtils.isNullOrEmpty(cumulativeError)) {
            throw new AssertionException(new AssertionError(cumulativeError));
        }
    }

    private void parseRelatesToNode(SoapVersion soapVersion, XmlObject requestXmlObject, Element relatesToNode) {
        if (relatesToNode == null) {
            cumulativeErrorMsg.append("WS-A RelatesTo property is not specified. ");
        } else {
            String relatesToValue = XmlUtils.getElementText(relatesToNode);
            if (StringUtils.isNullOrEmpty(relatesToValue)) {
                cumulativeErrorMsg.append("WS-A RelatesTo property is empty. ");
            } else {
                String requestMsgId = WsdlUtils.getRequestWsaMessageId(messageExchange,
                        getWsaVersion(requestXmlObject, soapVersion));
                if (!relatesToValue.equals(requestMsgId)) {
                    cumulativeErrorMsg.append("WS-A RelatesTo property is not equal to request wsa:MessageId. ");
                }
            }
            /*
			 * When absent, the implied value of this attribute is
			 * "http://www.w3.org/2005/08/addressing/reply". question is does it
			 * have to be present as 'reply' ???
			 */

            // String relationshipType =
            // relatesToNode.getAttribute("RelationshipType");
            // if (StringUtils.isNullOrEmpty(relationshipType))
            // {
            // relationshipType =
            // relatesToNode.getAttributeNS(WsaUtils.WS_A_VERSION_200508,
            // "RelationshipType");
            // if (StringUtils.isNullOrEmpty(relationshipType))
            // {
            // relationshipType =
            // relatesToNode.getAttributeNS(WsaUtils.WS_A_VERSION_200408,
            // "RelationshipType");
            // if (StringUtils.isNullOrEmpty(relationshipType))
            // {
            // cumulativeErrorMsg.append("WS-A RelationshipType is not specified. ");
            // }
            // }
            // }
        }
    }

    /*
     * asserts specific property
     */
    private void assertProperty(String propertyName, String wsaProperty, String expectedValue) {
        Element propertyNode;

        if (wsaVersionNameSpace != null) {
            propertyNode = XmlUtils.getFirstChildElementNS(header, wsaVersionNameSpace, wsaProperty);
            parsePropertyNode(propertyName, propertyNode, expectedValue);
        } else {
            propertyNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200508, wsaProperty);
            if (propertyNode == null) {
                propertyNode = XmlUtils.getFirstChildElementNS(header, WsaUtils.WS_A_NAMESPACE_200408, wsaProperty);
            }
            parsePropertyNode(propertyName, propertyNode, expectedValue);
        }
    }

    private void assertProperty(String propertyName, String wsaProperty) {
        assertProperty(propertyName, wsaProperty, null);
    }

    private void parsePropertyNode(String propertyName, Element propertyNode, String expectedValue) {
        if (propertyNode == null) {
            cumulativeErrorMsg.append(propertyName + " property is not specified. ");
        } else {
            String actionValue = XmlUtils.getElementText(propertyNode);
            if (StringUtils.isNullOrEmpty(actionValue)) {
                cumulativeErrorMsg.append(propertyName + " property is empty. ");
            } else if (!StringUtils.isNullOrEmpty(expectedValue)) {
                if (!actionValue.equals(expectedValue)) {
                    cumulativeErrorMsg.append(propertyName + " expecting [" + expectedValue + "], actual value is ["
                            + actionValue + "].");
                }
            }
        }
    }
}
